import type { Base } from "../Base";
import {
  cubeVertexArray,
  cubeVertexSize,
  cubePositionOffset,
  cubeUVOffset,
  cubeVertexCount,
} from "../../meshes/cube";
import { mat4, vec3 } from "wgpu-matrix";
import baseVertWGSL from "../../shaders/basic.vert.wgsl";
import vertPositionColorWGSL from "../../shaders/vertexPositionColor.frag.wgsl";
import { quitIfWebGPUNotAvailable } from "../../utils";
class OneCube implements Base {
  constructor() {
    // implementation goes here
  }
  async run(el: HTMLCanvasElement) {
    const adapter = await navigator.gpu.requestAdapter();
    const device = await adapter?.requestDevice();
    const context = el.getContext("webgpu") as GPUCanvasContext;
    quitIfWebGPUNotAvailable(adapter, device ?? null);

    if (!device) {
      throw new Error("WebGPU device is not available.");
    }
    const devicePixelRatio = window.devicePixelRatio;
    el.width = el.clientWidth * devicePixelRatio;
    el.height = el.clientHeight * devicePixelRatio;
    const format = navigator.gpu.getPreferredCanvasFormat();

    context?.configure({
      device,
      format,
    });

    const verticesBuffer = device.createBuffer({
      size: cubeVertexArray.byteLength,
      usage: GPUBufferUsage.VERTEX,
      mappedAtCreation: true,
    });
    new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
    verticesBuffer.unmap();

    const pipeline = device.createRenderPipeline({
      layout: "auto",
      vertex: {
        module: device.createShaderModule({
          code: baseVertWGSL,
        }),
        buffers: [
          {
            arrayStride: cubeVertexSize,
            attributes: [
              {
                // position

                shaderLocation: 0,
                offset: cubePositionOffset,
                format: "float32x3",
              },
              {
                // uv
                shaderLocation: 1,
                offset: cubeUVOffset,
                format: "float32x2",
              },
            ],
          },
        ],
      },
      fragment: {
        module: device.createShaderModule({
          code: vertPositionColorWGSL,
        }),
        targets: [
          {
            format: format,
          },
        ],
      },
      primitive: {
        topology: "triangle-list",
        // Backface culling since the cube is solid piece of geometry.
        // Faces pointing away from the camera will be occluded by faces
        // pointing toward the camera.
        cullMode: "back",
      },
      // Enable depth testing so that the fragment closest to the camera
      // is rendered in front.
      depthStencil: {
        format: "depth24plus",
        depthWriteEnabled: true,
        depthCompare: "less",
      },
    });

    const depthTexture = device.createTexture({
      size: [el.width, el.height],
      format: "depth24plus",
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const uniformBufferSize = 4 * 16; // 4*4 matrix
    const uniformBuffer = device.createBuffer({
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    const uniformBindGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        {
          binding: 0,
          resource: {
            buffer: uniformBuffer,
          },
        },
      ],
    });

    const renderPassDescriptor: GPURenderPassDescriptor = {
      colorAttachments: [
        {
          view: null as unknown as GPUTextureView,
          clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
          loadOp: "clear",
          storeOp: "store",
        },
      ] as GPURenderPassColorAttachment[],
      depthStencilAttachment: {
        view: depthTexture.createView(),
        depthClearValue: 1.0,
        depthLoadOp: "clear",
        depthStoreOp: "store",
      },
    };

    const aspect = el.width / el.height;
    const projectionMatrix = mat4.perspective(
      (2 * Math.PI) / 5,
      aspect,
      1,
      100
    );
    const modelViewProjectionMatrix = mat4.create();

    function getTranslateMatrix() {
      const viewMatrix = mat4.identity();
      mat4.translate(viewMatrix, vec3.fromValues(0, 0, -4), viewMatrix);
      const now = Date.now() / 1000;
      mat4.rotate(
        viewMatrix,
        vec3.fromValues(Math.sin(now), Math.cos(now), 0),
        1,
        viewMatrix
      );
      mat4.multiply(projectionMatrix, viewMatrix, modelViewProjectionMatrix);

      return modelViewProjectionMatrix;
    }

    function frame() {
      const transformationMatrix = getTranslateMatrix();
      device?.queue.writeBuffer(
        uniformBuffer,
        0,
        transformationMatrix.buffer,
        transformationMatrix.byteOffset,
        transformationMatrix.byteLength
      );
      (
        renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[]
      )[0].view = context.getCurrentTexture().createView();

      const commandEncoder = device?.createCommandEncoder();
      const renderPassEncoder =
        commandEncoder?.beginRenderPass(renderPassDescriptor);
      renderPassEncoder?.setPipeline(pipeline);
      renderPassEncoder?.setBindGroup(0, uniformBindGroup);
      renderPassEncoder?.setVertexBuffer(0, verticesBuffer);
      renderPassEncoder?.draw(cubeVertexCount);
      renderPassEncoder?.end();
      if (commandEncoder) {
        const commandBuffer = commandEncoder.finish();
        device?.queue.submit([commandBuffer]);
      }

      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
  }
  destroy(): void {
    // to do
  }
}

export default OneCube;
